5.2 方法
方法能给用户定义的类型添加新的行为。方法实际上也是函数,只是在声明时,在关键字 func
和方法名之间增加了一个参数,如代码清单5-11所示。
代码清单5-11 listing11.go
01 // 这个示例程序展示如何声明
02 // 并使用方法
03 package main
04
05 import (
06 "fmt"
07 )
08
09 // user在程序里定义一个用户类型
10 type user struct {
11 name string
12 email string
13 }
14
15 // notify使用值接收者实现了一个方法
16 func (u user) notify() {
17 fmt.Printf("Sending User Email To %s<%s>\n",
18 u.name,
19 u.email)
20 }
21
22 // changeEmail使用指针接收者实现了一个方法
23 func (u *user) changeEmail(email string) {
24 u.email = email
25 }
26
27 // main是应用程序的入口
28 func main() {
29 // user类型的值可以用来调用
30 // 使用值接收者声明的方法
31 bill := user{"Bill", "bill@email.com"}
32 bill.notify()
33
34 // 指向user类型值的指针也可以用来调用
35 // 使用值接收者声明的方法
36 lisa := &user{"Lisa", "lisa@email.com"}
37 lisa.notify()
38
39 // user类型的值可以用来调用
40 // 使用指针接收者声明的方法
41 bill.changeEmail("bill@newdomain.com")
42 bill.notify()
43
44 // 指向user类型值的指针可以用来调用
45 // 使用指针接收者声明的方法
46 lisa.changeEmail("lisa@newdomain.com")
47 lisa.notify()
48 }
代码清单5-11的第16行和第23行展示了两种类型的方法。关键字 func
和函数名之间的参数被称作 接收者 ,将函数与接收者的类型绑在一起。如果一个函数有接收者,这个函数就被称为 方法 。当运行这段程序时,会得到代码清单5-12所示的输出。
代码清单5-12 listing11.go的输出
Sending User Email To Bill<bill@email.com>
Sending User Email To Lisa<lisa@email.com>
Sending User Email To Bill<bill@newdomain.com>
Sending User Email To Lisa<lisa@comcast.com>
让我们来解释一下代码清单5-13所示的程序都做了什么。在第10行,该程序声明了名为 user
的结构类型,并声明了名为 notify
的方法。
代码清单5-13 listing11.go:第09行到第20行
09 // user在程序里定义一个用户类型
10 type user struct {
11 name string
12 email string
13 }
14
15 // notify使用值接收者实现了一个方法
16 func (u user) notify() {
17 fmt.Printf("Sending User Email To %s<%s>\n",
18 u.name,
19 u.email)
20 }
Go语言里有两种类型的接收者: 值接收者 和 指针接收者 。在代码清单5-13的第16行,使用值接收者声明了 notify
方法,如代码清单5-14所示。
代码清单5-14 使用值接收者声明一个方法
func (u user) notify() {
notify
方法的接收者被声明为 user
类型的值。如果使用值接收者声明方法,调用时会使用这个值的一个副本来执行。让我们跳到代码清单5-11的第32行来看一下如何调用 notify
方法,如代码清单5-15所示。
代码清单5-15 listing11.go:第29行到第32行
29 // user类型的值可以用来调用
30 // 使用值接收者声明的方法
31 bill := user{"Bill", "bill@email.com"}
32 bill.notify()
代码清单5-15展示了如何使用 user
类型的值来调用方法。第31行声明了一个 user
类型的变量 bill
,并使用给定的名字和电子邮件地址做初始化。之后在第32行,使用变量 bill
来调用 notify
方法,如代码清单5-16所示。
代码清单5-16 使用变量来调用方法
bill.notify()
这个语法与调用一个包里的函数看起来很类似。但在这个例子里, bill
不是包名,而是变量名。这段程序在调用 notify
方法时,使用 bill
的值作为接收者进行调用,方法 notify
会接收到 bill
的值的一个副本。
也可以使用指针来调用使用值接收者声明的方法,如代码清单5-17所示。
代码清单5-17 listing11.go:第34行到第37行
34 // 指向user类型值的指针也可以用来调用
35 // 使用值接收者声明的方法
36 lisa := &user{"Lisa", "lisa@email.com"}
37 lisa.notify()
代码清单5-17展示了如何使用指向 user
类型值的指针来调用 notify
方法。在第36行,声明了一个名为 lisa
的指针变量,并使用给定的名字和电子邮件地址做初始化。之后在第37行,使用这个指针变量来调用 notify
方法。为了支持这种方法调用,Go语言调整了指针的值,来符合方法接收者的定义。可以认为Go语言执行了代码清单5-18所示的操作。
代码清单5-18 Go在代码背后的执行动作
(*lisa).notify()
代码清单5-18展示了Go编译器为了支持这种方法调用背后做的事情。指针被解引用为值,这样就符合了值接收者的要求。再强调一次, notify
操作的是一个副本,只不过这次操作的是从 lisa
指针指向的值的副本。
也可以使用指针接收者声明方法,如代码清单5-19所示。
代码清单5-19 listing11.go:第22行到第25行
22 // changeEmail使用指针接收者实现了一个方法
23 func (u *user) changeEmail(email string) {
24 u.email = email
25 }
代码清单5-19展示了 changeEmail
方法的声明。这个方法使用指针接收者声明。这个接收者的类型是指向 user
类型值的指针,而不是 user
类型的值。当调用使用指针接收者声明的方法时,这个方法会共享调用方法时接收者所指向的值,如代码清单5-20所示。
代码清单5-20 listing11.go:第36行和第44行到第46行
36 lisa := &user{"Lisa", "lisa@email.com"}
44 // 指向user类型值的指针可以用来调用
45 // 使用指针接收者声明的方法
46 lisa.changeEmail("lisa@newdomain.com")
在代码清单5-20中,可以看到声明了 lisa
指针变量,还有第46行使用这个变量调用了 changeEmail
方法。一旦 changeEmail
调用返回,这个调用对值做的修改也会反映在 lisa
指针所指向的值上。这是因为 changeEmail
方法使用了指针接收者。总结一下,值接收者使用值的副本来调用方法,而指针接收者使用实际值来调用方法。
也可以使用一个值来调用使用指针接收者声明的方法,如代码清单5-21所示。
代码清单5-21 listing11.go:第31行和第39行到第41行
31 bill := user{"Bill", "bill@email.com"}
39 // user类型的值可以用来调用
40 // 使用指针接收者声明的方法
41 bill.changeEmail("bill@newdomain.com")
在代码清单5-21中可以看到声明的变量 bill
,以及之后使用这个变量调用使用指针接收者声明的 changeEmail
方法。Go语言再一次对值做了调整,使之符合函数的接收者,进行调用,如代码清单5-22所示。
代码清单5-22 Go在代码背后的执行动作
(&bill).changeEmail("bill@newdomain.com")
代码清单5-22展示了Go编译器为了支持这种方法调用在背后做的事情。在这个例子里,首先引用 bill
值得到一个指针,这样这个指针就能够匹配方法的接收者类型,再进行调用。Go语言既允许使用值,也允许使用指针来调用方法,不必严格符合接收者的类型。这个支持非常方便开发者编写程序。
应该使用值接收者,还是应该使用指针接收者,这个问题有时会比较迷惑人。可以遵从标准库里一些基本的指导方针来做决定。后面会进一步介绍这些指导方针。